package com.sp.business.normalization.impl.mf2;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import com.sp.business.normalization.impl.mf2.Mf2Lexer.Token;
import com.sp.business.normalization.impl.mf2.Mf2Lexer.TokenType;

/**
 * This class is a recursive descent parser for a subset of Lua
 * (http://www.lua.org/). The conf files contain Lua tables
 * (http://www.lua.org/pil/2.5.html). The Mf2Object implements the table
 * behavior in Java. This parser builds tables (Mf2Objects) recursively.
 * 
 * EBNF Grammar
 * 
 * object:= LABEL block
 * 
 * objList:= (object ';')*
 * 
 * block:= '{' (expList|objList)? '}'
 * 
 * expList:= exp (, exp)*
 * 
 * exp:=  empty 
 *      | (boolean|digit|string|block) 
 *      | label '=' (boolean|digit|string|block)
 *      | '['digit']' '=' (boolean|digit|string|block) 
 *      | '['string']' '=' (boolean|digit|string|block)
 * 
 * @author mtaylor
 * 
 */
public class Mf2Parser {
    private Stack<Mf2Object> stack;
    private List<Token> tokens;
    private int currToken;

    public Mf2Parser() {
        stack = new Stack<Mf2Object>();
    }
    
    public List<Mf2Object> parseObjList(BufferedReader reader) throws IOException {
        Mf2Lexer lexer = new Mf2Lexer();
        tokens = lexer.tokenize(reader);
        currToken = 0;
        
        List<Mf2Object> list = new ArrayList<Mf2Object>();
        while (peek() != null && (TokenType.LABEL).equals(peek().type)) {
            Mf2Object object = parseObject();
            list.add(object);
        }
        return list;
    }

    public Mf2Object parse(BufferedReader reader) throws IOException {
        Mf2Lexer lexer = new Mf2Lexer();
        tokens = lexer.tokenize(reader);
        currToken = 0;

        return parseObject();
    }

    private Token expect(TokenType type) {
        Token token = tokens.get(currToken);
        if ((type).equals(token.type)) {
            currToken++;
        } else {
            throw new RuntimeException("Expected " + type.name()
                    + ", but found " + token.type.name());
        }
        //System.out.println("expect: " + token.text);
        return token;
    }

    private Token nextToken() {
        Token token = tokens.get(currToken);
        currToken++;
        //System.out.println("expect: " + token.text);
        return token;
    }

    private Token peek() {
        return peek(0);
    }

    private Token peek(int offset) {
        int index = currToken + offset;
        if(index < tokens.size()) {
            return tokens.get(index);
        } else {
            return null;
        }
    }

    // LABEL block
    private Mf2Object parseObject() {
        Token label = expect(TokenType.LABEL);
        // create a new scope and push onto sack
        Mf2Object mf2Obj = new Mf2Object();
        mf2Obj.label = label.text;
        stack.push(mf2Obj);
        parseBlock();
        return stack.pop();
    }

    // (object ';')*
    private void parseObjList() {
        // parseObject();
        while ((TokenType.LABEL).equals(peek().type)) {
            Mf2Object object = parseObject();
            stack.peek().tempValue = object;
            stack.peek().commit();
            expect(TokenType.DELIMITER);
        }
    }

    // '{' expList '}'
    private void parseBlock() {
        expect(TokenType.LT_BRACE);
        if (peek(1) != null && (TokenType.LT_BRACE).equals(peek(1).type)) {
            parseObjList();
        } else {
            parseExpList();
        }
        expect(TokenType.RT_BRACE);
    }

    // exp (, exp)*
    private void parseExpList() {
        parseExp();
        while ((TokenType.DELIMITER).equals(peek().type)) {
            expect(TokenType.DELIMITER);
            parseExp();
        }
    }

    // empty
    // boolean|digit|string|block
    // label = boolean|digit|string|block
    // [digit] = boolean|digit|string|block
    // ["..."] = boolean|digit|string|block
    private void parseExp() {
        // get the key if there is one
        parseLVal();
        // get the value
        if ((TokenType.LT_BRACE).equals(peek().type)) {
            // create a new scope and push onto sack
            Mf2Object mf2Obj = new Mf2Object();
            stack.push(mf2Obj);
            parseBlock();
            Mf2Object obj = stack.pop();
            stack.peek().tempValue = obj;
        } else if ((TokenType.BOOLEAN).equals(peek().type)) {
            Boolean boolVal = Boolean.valueOf(nextToken().text);
            stack.peek().tempValue = boolVal;
        } else if ((TokenType.DIGIT).equals(peek().type)) {
            Integer intVal = Integer.valueOf(nextToken().text);
            stack.peek().tempValue = intVal;
        } else if ((TokenType.STRING).equals(peek().type)) {
            stack.peek().tempValue = nextToken().text;
        }
        // now we have a key, value saved in the Mf2Object,
        // commit() actually adds it to the array OR the map
        stack.peek().commit();
    }

    private void parseLVal() {
        Object key = null;
        if ((TokenType.LABEL).equals(peek().type)) {
            key = nextToken().text;
            expect(TokenType.EQUAL);
        } else if ((TokenType.LT_BRACKET).equals(peek().type)) {
            expect(TokenType.LT_BRACKET);
            Token t = nextToken();
            if ((TokenType.DIGIT).equals(t.type)) {
                key = Integer.valueOf(t.text);
            } else if ((TokenType.STRING).equals(t.type)) {
                key = t.text;
            }
            expect(TokenType.RT_BRACKET);
            expect(TokenType.EQUAL);
        }
        stack.peek().tempKey = key;
    }
}
